Repository navigation
Onboarded Model Experiments service on Stackit CLI - #1587
haya-fatima wants to merge 14 commits into
Conversation
|
This PR was marked as stale after 7 days of inactivity and will be closed after another 7 days of further inactivity. If this PR should be kept open, just add a comment, remove the stale label or push new commits to it. |
|
|
||
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "ai-model-experiments", |
There was a problem hiding this comment.
Is there a specific reason why command is named as "ai-model-experiments"? I would prefer here we call it "model-experiments", as we onboarded terraform resources also like that. We can mention in descriptions, in short long fields as AI Model Experiment.
I saw that in Portal it is called AI Model Experiment and we generally align with them, but then we will differ from SDK and Terraform Provider.
| github.com/stackitcloud/stackit-sdk-go/services/iaas v1.14.5 | ||
| github.com/stackitcloud/stackit-sdk-go/services/intake v0.11.2 | ||
| github.com/stackitcloud/stackit-sdk-go/services/logs v0.10.2 | ||
| github.com/stackitcloud/stackit-sdk-go/services/modelexperiments v0.3.0 |
There was a problem hiding this comment.
we can onboard the newer version v0.3.1
| ) | ||
|
|
||
| func ConfigureClient(p *print.Printer, cliVersion string) (*modelexperiments.APIClient, error) { | ||
| return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.AIModelExperimentsCustomEndpointKey), false, genericclient.CreateApiClient[*modelexperiments.APIClient](modelexperiments.NewAPIClient)) |
There was a problem hiding this comment.
| return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.AIModelExperimentsCustomEndpointKey), false, genericclient.CreateApiClient[*modelexperiments.APIClient](modelexperiments.NewAPIClient)) | |
| return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.AIModelExperimentsCustomEndpointKey), false, modelexperiments.NewAPIClient) |
| viper.SetDefault(LogsCustomEndpointKey, "") | ||
| viper.SetDefault(CDNCustomEndpointKey, "") | ||
| viper.SetDefault(ValkeyCustomEndpointKey, "") | ||
| viper.SetDefault(VPNCustomEndpointKey, "") |
There was a problem hiding this comment.
We should add it also here the endpoint key
| viper.SetDefault(LogsCustomEndpointKey, "") | ||
| viper.SetDefault(CDNCustomEndpointKey, "") | ||
| viper.SetDefault(ValkeyCustomEndpointKey, "") | ||
| viper.SetDefault(VPNCustomEndpointKey, "") |
There was a problem hiding this comment.
| viper.SetDefault(AIModelExperimentsCustomEndpointKey, "") |
There was a problem hiding this comment.
If It is renamed to ModelExperiments, please adapt the suggestion
| if resp == nil { | ||
| return "", ErrResponseNil | ||
| } | ||
| if resp.Instance.Name == "" { |
There was a problem hiding this comment.
You can combine this check with the above one, like you have done in the GetTokenName function.
There was a problem hiding this comment.
| if resp.Instance.Name == "" { | |
| if resp == nil || resp.Instance.Name == "" { | |
| return "", ErrResponseNil | |
| } | |
| return resp.Instance.Name, nil |
|
|
||
| var ( | ||
| testProjectID = uuid.NewString() | ||
| testRegion = "eu01" |
There was a problem hiding this comment.
This is a const and can be defined as
const (
testRegion = "eu01"
)
| ) | ||
|
|
||
| type mockSettings struct { | ||
| instanceFails bool |
There was a problem hiding this comment.
getInstanceFails would be more understandable.
This is also valid for the following vars
getInstanceResp
getTokenFails
getTokenResp
|
|
||
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: fmt.Sprintf("get %s", instanceIdArg), |
There was a problem hiding this comment.
as I already commented in the instance.go, command must be called "describe"
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: fmt.Sprintf("get %s", instanceIdArg), | ||
| Short: "Gets an AI Model Experiments instance", |
There was a problem hiding this comment.
| Short: "Gets an AI Model Experiments instance", | |
| Short: "Shows details of an AI Model Experiments instance", |
| Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Get an AI Model Experiments instance with ID "xxx"`, |
There was a problem hiding this comment.
| `Get an AI Model Experiments instance with ID "xxx"`, | |
| `Get details of an AI Model Experiments instance with ID "xxx"`, |
| examples.NewExample( | ||
| `Get an AI Model Experiments instance with ID "xxx"`, | ||
| `$ stackit ai-model-experiments instance get xxx`, | ||
| ), |
There was a problem hiding this comment.
For clarity, Please add here one more example which is using the output flag
There was a problem hiding this comment.
| ), | |
| ), | |
| examples.NewExample( | |
| `Get details of an AI Model Experiments instance with ID "xxx" in JSON format`, | |
| `$ stackit ai-model-experiments instance get xxx --output-format json`), |
| return &model, nil | ||
| } | ||
|
|
||
| func buildGetInstanceRequest( |
There was a problem hiding this comment.
nitpick: function can be called buildRequest
| model.Region, | ||
| model.InstanceId, | ||
| ) | ||
| } |
There was a problem hiding this comment.
This might be a nitpick, but I think it will help a lot for the readability and uniformity across the cli. Please use the same indentations and single/multi line styling in return and function headers
There was a problem hiding this comment.
| } | |
| func buildRequest(ctx context.Context, model *inputModel, apiClient *modelexperiments.APIClient) modelexperiments.ApiGetInstanceRequest { | |
| return apiClient.DefaultAPI.GetInstance(ctx, model.ProjectId, model.Region, model.InstanceId) | |
| } |
|
|
||
| return p.OutputResult(outputFormat, resp.Instance, func() error { | ||
| table := tables.NewTable() | ||
| table.SetHeader("ID", "NAME", "REGION", "STATUS") |
There was a problem hiding this comment.
In the describe command output, we create table differently than the list command output. Please take a look at the other describe command output functions for example.
Besides that, in describe command we expect a more detailed output for the user. In the table view, you can also provide the same information shown in the json or yaml outputs.
There was a problem hiding this comment.
| table.SetHeader("ID", "NAME", "REGION", "STATUS") | |
| func outputResult(p *print.Printer, outputFormat string, resp *modelexperiments.GetInstanceResponse) error { | |
| if resp == nil { | |
| return fmt.Errorf("response instance is nil") | |
| } | |
| return p.OutputResult(outputFormat, resp.Instance, func() error { | |
| table := tables.NewTable() | |
| table.AddRow("ID", resp.Instance.Id) | |
| table.AddSeparator() | |
| table.AddRow("NAME", resp.Instance.Name) | |
| table.AddSeparator() | |
| table.AddRow("REGION", utils.PtrString(resp.Instance.Region)) | |
| table.AddSeparator() | |
| table.AddRow("STATUS", resp.Instance.State) | |
| table.AddSeparator() | |
| err := table.Display(p) | |
| if err != nil { | |
| return fmt.Errorf("render table: %w", err) | |
| } | |
| return nil | |
| }) | |
| } |
| "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" | ||
| ) | ||
|
|
||
| var projectIdFlag = globalflags.ProjectIdFlag |
There was a problem hiding this comment.
nitpick: you can use this directly as globalflags.ProjectIdFlag
| } | ||
|
|
||
| var testProjectId = uuid.NewString() | ||
| var testInstanceId = uuid.NewString() |
There was a problem hiding this comment.
| var testInstanceId = uuid.NewString() | |
| var ( | |
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | |
| testClient = &modelexperiments.APIClient{DefaultAPI: modelexperiments.DefaultAPIServiceMock{}} | |
| testProjectId = uuid.NewString() | |
| testInstanceId = uuid.NewString() | |
| ) |
|
|
||
| var testProjectId = uuid.NewString() | ||
| var testInstanceId = uuid.NewString() | ||
|
|
There was a problem hiding this comment.
Region value has been used across the tests. Here you can define a const and use that one.
const testRegion = "eu01"
|
|
||
| return flagValues | ||
| } | ||
|
|
There was a problem hiding this comment.
Here you can define a fixtureArgValues function and use this in the following tests argValues fields. In case you need to make a change, it will be simpler.
There was a problem hiding this comment.
| func fixtureArgValues(mods ...func(argValues []string)) []string { | |
| argValues := []string{ | |
| testInstanceId, | |
| } | |
| for _, mod := range mods { | |
| mod(argValues) | |
| } | |
| return argValues | |
| } |
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Region: "eu01", |
There was a problem hiding this comment.
as I mentioned above, if you define a const, you can use it in here
| var testInstanceId = uuid.NewString() | ||
|
|
||
| func fixtureFlagValues( | ||
| mods ...func(*map[string]string), |
There was a problem hiding this comment.
why do you use pointer here *map[string]string?
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, |
There was a problem hiding this comment.
Please add more unit tests here. You can see in other describe commands some example test cases you can add
There was a problem hiding this comment.
some examples
{
description: "no values",
argValues: []string{},
flagValues: map[string]string{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
| } | ||
| } | ||
|
|
||
| func TestBuildGetInstanceRequest(t *testing.T) { |
There was a problem hiding this comment.
| func TestBuildGetInstanceRequest(t *testing.T) { | |
| func TestBuildGetInstanceRequest(t *testing.T) { | |
| tests := []struct { | |
| description string | |
| model *inputModel | |
| expectedRequest modelexperiments.ApiGetInstanceRequest | |
| }{ | |
| { | |
| description: "base", | |
| model: fixtureInputModel(), | |
| expectedRequest: fixtureRequest(), | |
| }, | |
| } | |
| for _, tt := range tests { | |
| t.Run(tt.description, func(t *testing.T) { | |
| request := buildRequest(testCtx, tt.model, testClient) | |
| diff := cmp.Diff(request, tt.expectedRequest, | |
| cmp.AllowUnexported(tt.expectedRequest), | |
| cmpopts.EquateComparable(testCtx), | |
| ) | |
| if diff != "" { | |
| t.Fatalf("Data does not match: %s", diff) | |
| } | |
| }) | |
| } | |
| } |
There was a problem hiding this comment.
for following the similar coding style to the existing tests
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "list", | ||
| Short: "Lists AI Model Experiments instances", |
There was a problem hiding this comment.
| Short: "Lists AI Model Experiments instances", | |
| Short: "Lists all AI Model Experiments instances", |
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `List AI Model Experiments instances in a project`, | ||
| `$ stackit ai-model-experiments instance list --region eu01`, |
There was a problem hiding this comment.
You can give here multiple examples. One simple one without mentioning region flag. Region is a global flag, you dont really need to mention. But if you want to mention in your example, maybe give it in separate example and by mentioning .... in a project in region eu02 e.g
BEsides that one example with the output format would be nice. You can see examples in other list commands
| return cmd | ||
| } | ||
|
|
||
| func parseInput( |
There was a problem hiding this comment.
the comments I left in other scripts about function headers are valid across whole PR. :)
|
|
||
| modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" | ||
| ) | ||
|
|
There was a problem hiding this comment.
In list commands, we use the limit flag to set the max number of entries to list. Please extend this. Examples can be found in other list commands
|
|
||
| return p.OutputResult(outputFormat, resp.Instances, func() error { | ||
| if len(resp.Instances) == 0 { | ||
| p.Outputf("No instances found\n") |
There was a problem hiding this comment.
| p.Outputf("No instances found\n") | |
| p.Outputf("No instances found for project %q\n", projectLabel) |
There was a problem hiding this comment.
I think mentioning the project name would be a nicer output. You need to parse the projectLabel to outputResult function call.
| } | ||
|
|
||
| table := tables.NewTable() | ||
| table.SetHeader("ID", "NAME", "REGION", "STATUS") |
There was a problem hiding this comment.
Portal shows in list view bucket name as "ARTIFACT STORAGE NAME", we can also add into here.
The dashboard link, might be skipped in my opinion.
| table.AddRow( | ||
| instance.Id, | ||
| instance.Name, | ||
| *instance.Region, |
There was a problem hiding this comment.
| *instance.Region, | |
| utils.PtrString(instance.Region), |
| return &model, nil | ||
| } | ||
|
|
||
| func buildListInstancesRequest( |
There was a problem hiding this comment.
| func buildListInstancesRequest( | |
| func buildRequest( |
| "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" | ||
| ) | ||
|
|
There was a problem hiding this comment.
Please see comments from the describe (get) command, about the var grouping, const region, pointer usage etc.
| }), | ||
| isValid: false, | ||
| }, | ||
| } |
There was a problem hiding this comment.
Also here tests can be extended
| const ( | ||
| nameFlag = "name" | ||
| descriptionFlag = "description" | ||
| labelFlag = "label" |
There was a problem hiding this comment.
We can define multiple labels as key=value pairs and seperating with ,'s . So it would make sense to call this as labelsFlag = "labels"
| `Create an instance with a description and labels`, | ||
| `$ stackit ai-model-experiments instance create --name my-tracking --description "team tracking server" --label env=prod`), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, args []string) error { |
There was a problem hiding this comment.
| RunE: func(cmd *cobra.Command, args []string) error { | |
| RunE: func(cmd *cobra.Command, _ []string) error { |
| if err != nil { | ||
| return fmt.Errorf("create AI Model Experiments instance: %w", err) | ||
| } | ||
|
|
There was a problem hiding this comment.
This is an async operation but wait handler using is missing here.
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().StringP(nameFlag, "n", "", "Instance name") | ||
| cmd.Flags().String(descriptionFlag, "", "Instance description") | ||
| cmd.Flags().StringToString(labelFlag, nil, `Labels as key-value pairs, e.g. "--label env=prod"`) |
There was a problem hiding this comment.
as I mentioned above, here we can provide multiple labels. We should adapt description also
| GlobalFlagModel: globalFlags, | ||
| Name: flags.FlagToStringValue(p, cmd, nameFlag), | ||
| Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), | ||
| Labels: labelsPtr, |
There was a problem hiding this comment.
Please check flags.FlagToStringToStringPointer function
Description
Onboarded the model experiments service to the STACKIT CLI and updated the relevant functionality. Added the necessary tests and verified the changes locally.
In case there is no issue present for your PR, please consider creating one.
At least please give us some description what you are trying to achieve and why your change is needed. -->
relates to #STACKITMLO-1718
Checklist
make fmtmake generate-docs(will be checked by CI)make test(will be checked by CI)make lint(will be checked by CI)